/** @file    NetworkChannelUdp.cpp
 *  @author  Alessandro Polo
 *  @version $Id: NetworkChannelUdp.cpp 2829 2010-08-02 20:45:47Z alex $
 *  @brief
 * File containing methods for the wosh::network::NetworkChannelUdp class.
 * The header for this class can be found in NetworkChannelUdp.h, check that file
 * for class description.
 ****************************************************************************/
/* Copyright (c) 2007-2010, WOSH - Wide Open Smart Home 
 * by Alessandro Polo - OpenSmartHome.com
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of the OpenSmartHome.com WOSH nor the
 *       names of its contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY Alessandro Polo ''AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL Alessandro Polo BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 ****************************************************************************/

 #include <framework/network/NetworkChannelUdp.h>

 #include <core/ObjectFactory.h>
 #include <core/PersistenceManager.h>
 #include <core/WoshKernel.h>
 #include <core/Thread.h>
 #include <stdio.h> //@bug BUG: TEMP TO BE REMOVED
 

using namespace std;
namespace wosh {
 namespace network {

 WOSH_REGISTER(wosh::network::NetworkChannelUdp, "wosh::NetworkChannel", 1.01, _static_NetworkChannelUdp)

//////////////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////// CONSTRUCTORS

NetworkChannelUdp::NetworkChannelUdp( const std::string& name )
	: wosh::NetworkChannel(name) {
	string nname = name;
	if ( name == "" ) nname = Utilities::format("NetworkChannelUdp#10lu", wosh::Utilities::randomLong());
	Object::setName(nname);
	Log.setContext(nname);

Log.setLevel( LOG_VERBOSE );

	this->msgBufferOut = new char[wosh::network::SocketUdp::MaxPacketLength];
	memset( this->msgBufferOut, '\0', wosh::network::SocketUdp::MaxPacketLength );

	this->targetAddressPort = "";
	this->connectedACK = false;
	this->waitingPing = 0;
	strcpy(this->pingRequestHeader, "PINGREQ");
	strcpy(this->pingResponseHeader, "PINGRES");

	this->udpSocket = NULL;
 }

NetworkChannelUdp::~NetworkChannelUdp() {
	disconnect();

	if ( isListening() )
		stopListening();

	deinit();

	delete [] this->msgBufferOut; this->msgBufferOut = NULL;
 }

//////////////////////////////////////////////////////////////////////////////////////////////// CONSTRUCTORS
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////// (UN)LISTEN

bool NetworkChannelUdp::init() {
	this->udpSocket = ObjectFactory::createTypeOf<wosh::network::SocketUdp>(false);
	if ( this->udpSocket == NULL ) return false;
	Log(LOG_VERBOSE, ":init() Created Socket of type %s", this->udpSocket->getClassName() );
	this->udpSocket->setIgnoreSelfDatagrams(true);
	this->udpSocket->setListener(this);
	return true;
 }

void NetworkChannelUdp::deinit() {
	if ( this->udpSocket == NULL ) return;
	delete this->udpSocket; this->udpSocket = NULL;
 }


WRESULT NetworkChannelUdp::listen( const std::string& addressport, unsigned long timeout ) {
	setBindingAddressPort(addressport);
	return listen(timeout);
 }

WRESULT NetworkChannelUdp::listen( unsigned long timeout ) {
	if ( this->udpSocket != NULL && this->udpSocket->isBinded() ) {
		Log(LOG_CRITICAL, ":listen() FAILED: Socket Already allocated & binded!" );
		return WRET_ERR_ILLEGAL_USE;
	 }
	if ( this->udpSocket == NULL && !init() ) {
		Log(LOG_CRITICAL, ":listen() FAILED: Creating Socket!" );
		return WRET_ERR_INTERNAL;
	 }
	string addressport = Properties.getValueOf( _NetworkChannel_KEY_BindAddress ).toString();
	bool set = this->udpSocket->setAddressPort(addressport);
	if ( !set ) {
		Log(LOG_CRITICAL, ":listen() FAILED Setting address '%s'..", addressport.c_str() );
		return WRET_ERR_PARAM;
	 }
	Log(LOG_VERBOSE, ":listen() Binding UDP socket to '%s'..", this->udpSocket->getAddressPort().c_str() );
	WRESULT ret = this->udpSocket->bindSocket(timeout);
	if ( WFAILED(ret) ) {
		Properties.transactionBeginWrite();
		Properties.update( _NetworkChannel_KEY_Listening, false );
		Properties.transactionEnd();
		Log(LOG_CRITICAL, ":listen() FAILED#%d Binding[%d] socket  to %s", ret, this->udpSocket->isBinded(), this->udpSocket->getAddressPort().c_str() );
		deinit();
		return WRET_ERR_INTERNAL;
	 }
	Properties.transactionBeginWrite();
	Properties.update( _NetworkChannel_KEY_Listening, true );
	Properties.transactionEnd();
	Log(LOG_INFO, ":listen() Socket Binded to %s", this->udpSocket->getAddressPort().c_str() );
	return WRET_OK;
 }

//////////////////////////////////////////////////////////////////////////////////////////////////////////////

WRESULT NetworkChannelUdp::stopListening( unsigned long timeout ) {
	if ( !this->udpSocket->isBinded() ) {
		Properties.transactionBeginWrite();
		Properties.update( _NetworkChannel_KEY_Listening, false );
		Properties.transactionEnd();
		Log(LOG_WARNING, ":stopListening() FAILED: NOT listening!" );
		return WRET_ERR_ILLEGAL_USE;
	 }
	WRESULT ret = this->udpSocket->unbindSocket(timeout);
	if ( WFAILED(ret) ) {
		Properties.transactionBeginWrite();
		Properties.update( _NetworkChannel_KEY_Listening, true );
		Properties.transactionEnd();
		Log(LOG_CRITICAL, ":stopListening() FAILED#%d UnBinding[%d] socket  from %s", ret, this->udpSocket->isBinded(), this->udpSocket->getAddressPort().c_str() );
		return WRET_ERR_INTERNAL;
	 }
	Properties.transactionBeginWrite();
	Properties.update( _NetworkChannel_KEY_Listening, false );
	Properties.transactionEnd();
	Log(LOG_INFO, ":stopListening() Socket UnBinded [%s]", this->udpSocket->getAddressPort().c_str() );
	return WRET_OK;
 }

////////////////////////////////////////////////////////////////////////////////////////////////// (UN)LISTEN
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////////////(DIS)CONNECT

WRESULT NetworkChannelUdp::connect( const std::string& addressport, unsigned long timeout ) {
	setTargetAddressPort(addressport);
	return connect(timeout);
 }

WRESULT NetworkChannelUdp::connect( unsigned long timeout ) {
	if ( this->udpSocket == NULL ) {
		Log(LOG_WARNING, ":connect() Socket Not  allocated!" );
		if ( init() ) {
			Log(LOG_WARNING, ":listen() FAILED: Creating Socket!" );
			return WRET_ERR_INTERNAL;
		 }
	 }
	this->connectedACK = false;
	std::string data = Utilities::format("CONNECT_w_%s", WoshKernel::getKernelName());
	Log(LOG_VERBOSE, ":connect() Connect-Handshaking to %s..", this->targetAddressPort.c_str() );
	int bs = this->udpSocket->sendDatagram( this->targetAddressPort, data.c_str(), data.size() );
	if ( bs != (int)data.size() ) {
		Properties.transactionBeginWrite();
		Properties.update( _NetworkChannel_KEY_Connected, false );
		Properties.transactionEnd();
		Log(LOG_WARNING, ":connect() FAILED sending connect-packet to %s [size: %d, send: %d]", this->targetAddressPort.c_str(), data.size(), bs );
		deinit();
		return WRET_ERR_INTERNAL;
	 }
	Log(LOG_INFO, ":connect() Sent connect-packet to %s [%d bytes]", this->targetAddressPort.c_str(), bs );
	if ( timeout > 0 ) {
		unsigned long i = 0;
		unsigned long i_max = timeout / 100;
		while( i++ < i_max ) {
			if ( this->connectedACK ) {
				Properties.transactionBeginWrite();
				Properties.update( _NetworkChannel_KEY_Connected, true );
				Properties.transactionEnd();
				Log(LOG_INFO, ":connect() Connected with %s [%lu secs]!", this->targetAddressPort.c_str(), i );
				return WRET_OK;
			 }
			ThreadImpl::sleepForMSec(100);
		 }
		Log(LOG_CRITICAL, ":connect() ERROR: Connect-Handshaking timed out! [%lu]", timeout );
	 }
	return WRET_OK;
 }

//////////////////////////////////////////////////////////////////////////////////////////////////////////////

WRESULT NetworkChannelUdp::disconnect( unsigned long ) {
	if ( !this->udpSocket->isBinded() ) {
		Log(LOG_WARNING, ":disconnect() Socket wasn't binded" );
		return WRET_ERR_PARAM;
	 }
	Log(LOG_VERBOSE, ":disconnect() Disconnect-Handshaking to %s..", this->targetAddressPort.c_str() );
	std::string data = Utilities::format("DISCONNECT_%s", WoshKernel::getKernelName());
	int bs = this->udpSocket->sendDatagram( this->targetAddressPort, data.c_str(), data.size() );
	if ( bs != (int)data.size() ) {
		Log(LOG_WARNING, ":disconnect() FAILED sending disconnect-packet to %s [size: %d, send: %d]", this->targetAddressPort.c_str(), data.size(), bs );
		return WRET_ERR_INTERNAL;
	 }
	Log(LOG_VERBOSE, ":disconnect() Sent disconnect-packet to %s [%d bytes]", this->targetAddressPort.c_str(), bs );
	Properties.transactionBeginWrite();
	Properties.update( _NetworkChannel_KEY_Connected, false );
	Properties.transactionEnd();
	this->connectedACK = false;
	this->waitingPing = 0;
	return WRET_OK;
 }

/////////////////////////////////////////////////////////////////////////////////////////////////(DIS)CONNECT
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////// CHANNEL METHODS

WRESULT NetworkChannelUdp::ping( unsigned long timeout ) {
	const int len = 32;
	char data[len];
	memset(data, 0, len);
	memcpy(data, pingRequestHeader, _NetworkChannelUdp_PingHeaderSize);

	this->waitingPing = 1;
	WRESULT ret = sendData( data, len );
	if ( WFAILED(ret) ) {
		this->waitingPing = 0;
		Log(LOG_WARNING, ":ping() ERROR Sending ping packet.." );
		return ret;
	 }
	if ( timeout == 0 ) return WRET_OK;
	timeout = (unsigned long)(timeout / 100) + 1;
	unsigned long i = 0;
	while( ++i < timeout ) {
		if ( this->waitingPing == 2 ) {
			this->waitingPing = 0;
			Log(LOG_INFO, ":ping() Succeded-Ping!" );
			return WRET_OK;
		 }
		ThreadImpl::sleepForMSec(100);
	 }
	this->waitingPing = 0;
	Log(LOG_WARNING, ":ping() ERROR, ping timed out! [%lu]", timeout );
	return WRET_ERR_INTERNAL;
 }

//////////////////////////////////////////////////////////////////////////////////////////////////////////////

WRESULT NetworkChannelUdp::pingReply( const std::string& source ) {
	const int len = 32;
	char data[len];
	memset(data, 0, len);
	memcpy(data, pingResponseHeader, _NetworkChannelUdp_PingHeaderSize);

	int bs = this->udpSocket->sendDatagram( source, data, len );
	if ( bs != len ) {
		Log(LOG_WARNING, ":pingReply() FAILED Replying to %s [size: %d, sent: %d]", source.c_str(), len, bs );
		return WRET_ERR_INTERNAL;
	 }
	Log(LOG_VERBOSE, ":pingReply() Replied to %s [%d bytes]", source.c_str(), bs );
	return WRET_OK;
 }

//////////////////////////////////////////////////////////////////////////////////////////////////////////////

WRESULT NetworkChannelUdp::connectReply( const std::string& source ) {
	std::string data = Utilities::format("CONNECTED_w_%s", WoshKernel::getKernelName());
	int bs = this->udpSocket->sendDatagram( source, data.c_str(), data.size() );
	if ( bs != (int)data.size() ) {
		Log(LOG_WARNING, ":connectReply() FAILED Replying to %s [size: %d, sent: %d]", source.c_str(), data.size(), bs );
		return WRET_ERR_INTERNAL;
	 }
	Log(LOG_VERBOSE, ":connectReply() Replied to %s [%d bytes]", source.c_str(), bs );
	return WRET_OK;
 }

//////////////////////////////////////////////////////////////////////////////////////////////////////////////

WRESULT NetworkChannelUdp::declarateWoshHost() {
	return connectReply(this->targetAddressPort);
 }

///////////////////////////////////////////////////////////////////////////////////////////// CHANNEL METHODS
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////// SENDING DATA

WRESULT NetworkChannelUdp::sendMessage( const wosh::Message* message ) {
	if ( message == NULL ) return WRET_ERR_PARAM;

	this->msgBufferOutMux.lock();
	int64 size = PersistenceManager::serializeTo( message, "XML", this->msgBufferOut, wosh::network::SocketUdp::MaxPacketLength, true );
//	printf("\nSerialized MESSAGE [%"PRId64"]:\n%s\nSerialized MESSAGE [%"PRId64"]\n", size, this->msgBufferOut, message->getID());
	if ( size <= 0 ) {
		this->msgBufferOutMux.unLock();
		Log(LOG_WARNING, ":sendMessage() Serialization ERROR, Size=%"PRId64, size );
		return WRET_ERR_INTERNAL;
	 }
	int bs = this->udpSocket->sendDatagram( this->targetAddressPort, this->msgBufferOut, size );
	setTransmittedNow( (bs == (int)size) );
	this->msgBufferOutMux.unLock();
	if ( bs != (int)size ) {
		Log(LOG_WARNING, ":sendMessage() FAILED Sending to %s [size: %d, sent: %d]", this->targetAddressPort.c_str(), size, bs );
		return WRET_ERR_INTERNAL;
	 }
	Log(LOG_VERBOSE, ":sendMessage(%"PRId64") sent to %s [%d bytes]", message->getID(), this->targetAddressPort.c_str(), size );
	return WRET_OK;
 }

WRESULT NetworkChannelUdp::sendData( const char* data, unsigned long size ) {
	int bs = this->udpSocket->sendDatagram( this->targetAddressPort, data, size );
	if ( bs != (int)size ) {
		Log(LOG_WARNING, ":sendData() FAILED Sending to %s [size: %d, sent: %d]", this->targetAddressPort.c_str(), size, bs );
		return WRET_ERR_INTERNAL;
	 }
	Log(LOG_VERBOSE, ":sendData() Sent to %s [%d bytes]", this->targetAddressPort.c_str(), bs );
	return WRET_OK;
 }

//////////////////////////////////////////////////////////////////////////////////////////////// SENDING DATA
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////// UDP EVENTs

void NetworkChannelUdp::udpIncomingDatagram( const char * data, int maxSize, const std::string& source, wosh::network::SocketUdp* ) {
	// is that a connect packet?
	if ( strncmp(data, "CONNECT_w_", 10) == 0 ) {
		// the other side of the channel is trying to connect us.
		// we update internal status
		this->connectedACK = true;
		// we probably are in (binded) IDLE status, so let's set our target address
		this->targetAddressPort = source;
		Properties.transactionBeginWrite();
		Properties.update( _NetworkChannel_KEY_Connected, true );
		Properties.update( _NetworkChannel_KEY_ConnectAddress, this->targetAddressPort );
		Properties.transactionEnd();
		this->remoteWoshName = "";
		this->remoteWoshName.assign(data+10);
		Log(LOG_INFO, ":udpIncomingDatagram() Connected! Received handshake (request) from %s [%s] (replying..)", this->remoteWoshName.c_str(), source.c_str() );
		// and ACK request
		connectReply( source );
		// let's notify our listner that we are connected with -HOST-
		if ( this->listener != NULL )
			this->listener->channel_Connected(this, this->remoteWoshName);
		return;
	 }
	// is that a connect-reply packet? (ACK)
	else if ( strncmp(data, "CONNECTED_w_", 12) == 0 ) {
		// we started handshaking, here the other host replied OK.
		// we ensure status is synchronized
		this->connectedACK = true;
		Properties.transactionBeginWrite();
		Properties.update( _NetworkChannel_KEY_Connected, true );
		Properties.transactionEnd();
		// let's notify our listner that we are connected with -HOST-,
		// probably it does already know that, since usually connect() is called synch
		this->remoteWoshName = "";
		this->remoteWoshName.assign(data+12);
		Log(LOG_VERBOSE, ":udpIncomingDatagram() Connected! Received handshake (response) from %s [%s]", this->remoteWoshName.c_str(), source.c_str() );
		if ( this->listener != NULL )
			this->listener->channel_Connected(this, this->remoteWoshName);
		return;
	 }
	else if ( strncmp(data, "DISCONNECT_", 11) == 0 ) {
		// other side of the channel is closing,
		// synch status
		this->connectedACK = false;
		Properties.transactionBeginWrite();
		Properties.update( _NetworkChannel_KEY_Connected, false );
		Properties.transactionEnd();
		// let's notify our listener the new event
		std::string source_host = "";
		source_host.assign(data+11);
		if ( this->listener != NULL )
			this->listener->channel_Disconnected(this, source_host);
		return;
	 }
	// is that a ping-request packet?
	else if ( strncmp(data, pingRequestHeader, _NetworkChannelUdp_PingHeaderSize) == 0 ) {
		Log(LOG_VERBOSE, ":udpIncomingDatagram() Ping-Request from %s [%d]", source.c_str(), maxSize);
		// let's reply :)
		pingReply( source );
		return;
	 }
	// is that a ping-request/response-packet?
	else if ( strncmp(data, pingResponseHeader, _NetworkChannelUdp_PingHeaderSize) == 0 ) {
		if ( this->waitingPing == 0 )
			Log(LOG_WARNING, ":udpIncomingDatagram() Unwaited Ping Reply from %s [%d]", source.c_str(), maxSize);
		else
			Log(LOG_VERBOSE, ":udpIncomingDatagram() Ping-Response from %s [%d]", source.c_str(), maxSize);
		this->waitingPing = 2;
		return;
	 }
	Log(LOG_VERBOSE, ":udpIncomingDatagram() from %s [%d]", source.c_str(), maxSize);
	if ( this->listener == NULL ) {
		Log(LOG_WARNING, ":udpIncomingDatagram() NULL Listener!" );
		return;
	 }
	// deserialize
	wosh::Message* message = new wosh::Message();
	int64 ret = PersistenceManager::deserializeFrom( message, "XML", data, maxSize );
	setReceivedNow( (ret > 0) );
//	printf("\nDeSerialized MESSAGE [%"PRId64"]:\n%s\n( DeSerialized MESSAGE [%"PRId64"] )\n", ret, data, message->getID());

	if ( ret <= 0 ) {
		Log(LOG_WARNING, ":udpIncomingDatagram() Message::DeSerialization ERROR" );
		printf( "\nTEMP-DEBUG NetworkChannelTcp::parseDataMessage() size: %d\n%s\n", (int)maxSize, data );
		delete message;
	 }
	message->setSourceUUID( this->getEntityID() ); // cos of flooding
	this->listener->channel_incomingMessage( this, message ); // inject
 }


////////////////////////////////////////////////////////////////////////////////////////////////// UDP EVENTs
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////// GETTERS

bool NetworkChannelUdp::isConnected() const {
	return ( isListening() && this->connectedACK );
 }

bool NetworkChannelUdp::isListening() const {
	return ( this->udpSocket != NULL && this->udpSocket->isBinded() );
 }

std::string NetworkChannelUdp::getProtocol() const {
	if ( this->udpSocket == NULL ) return "UDP://";
	return "UDP://" + this->udpSocket->getAddressPort();
 }

///////////////////////////////////////////////////////////////////////////////////////////////////// GETTERS
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////// SETTERS

WRESULT NetworkChannelUdp::setBindingAddressPort( const std::string& address_port ) {
	if ( isListening() ) return WRET_ERR_PARAM;
	Properties.transactionBeginWrite();
	Properties.update( _NetworkChannel_KEY_BindAddress, address_port );
	Properties.transactionEnd();
	return WRET_OK;
 }

///////////////////////////////////////////////////////////////////////////////////////////////////// SETTERS
//////////////////////////////////////////////////////////////////////////////////////////////////////////////

//////////////////////////////////////////////////////////////////////////////////////////////////////////////

 }; // namespace network
}; // namespace wosh
